#!/usr/bin/env ruby

$:.push("libs")
require 'socket'
require "kdialog-0.3"

#make your own child class of AmaroKSlots to send to AmaroKLib.
module AmaroKSlots
	def AmaroKSlots.configure		
        	`dcop amarok playlist popupMessage \"This script does not have configuration options.\"`
	end
	def AmaroKSlots.stateChange_empty
		nil
	end	
	def AmaroKSlots.stateChange_idle
		nil
	end
	def AmaroKSlots.stateChange_paused
		nil
	end
	def AmaroKSlots.stateChange_playing
		nil
	end
	def AmaroKSlots.trackChange
		track_change()
	end
	#not a command given by amaroK, used when debugging
	def exit
		nil
	end
end

def track_change()
	song_file = dcop("player path")
	response = send_recv("\"get\" \"#{song_file.chomp}\"")		#don't use get_song because we already have the right path
	
	response_data = split_quotes(response)

	s = Musink_Song.new(song_file, response_data[1], response_data[2],
	                    response_data[3], response_data[4], response_data[5],
	                    response_data[6], response_data[7], response_data[8])

	s.increment_playcount
	send_song(s)
end

class Musink_Song
	def initialize( filepath,artist,album,title,duration,hash,playcount,rating,last_played)
		@artist = artist
		@album = album
		@title = title
		@duration = duration
		@hash = hash
		@playcount = playcount
		@rating = rating
		@last_played = last_played
		@filepath = filepath

		if (playcount == "") or (playcount == nil)
			@playcount = 0
		else
			@playcount = playcount.to_i
		end
		if rating  == "" or rating == nil
			@rating = 0.0
		end
	end

	def filepath
		@filepath
	end
	
	def set_filepath(f)
		@filepath = f
	end
	
	def artist
		@artist
	end
  
	def set_artist(a)
		@artist = a
  	end

	def album
		@album
	end
  
	def set_album(a)
		@album = a
	end

	def title
		@title
	end
  
	def set_title(t)
		@title = t
	end

	def duration
		@duration
	end
  
	def set_duration(d)
		@duration = d
	end
  
	def hash
		@hash
	end
  
	def set_hash(h)
		@hash = h
  	end

	def playcount
		@playcount
	end
  
	def rating
		@rating
	end
  
	def set_rating (r)
		@rating=r
	end
  
	def last_played
		@last_played
	end
   
	def set_last_played(lp)
		@last_played = lp
	end
	
	def increment_playcount
		if (@playcount != nil )
			@playcount = @playcount.to_i + 1
		else
			@playcount = 1
		end
		@last_played = Time.new.strftime("%Y%m%d %H:%M:%S")
	end
  
	def increase_playcount (pc)
		@playcount = @playcount + pc
	end
  
	def to_s
		return "Song Info: \r\n\tArtist: #{artist}\r\n\tAlbum: #{@album}\r\n\tTitle: #{@title}\r\n\tPlay Count: #{playcount}\r\n\tRating: #{rating}"
	end
  
	def to_response
		return  "\"#{filepath.chomp}\" \"#{artist.chomp}\" \"#{album.chomp}\" \"#{title.chomp}\" \"#{duration}\" \"#{hash}\" \"#{playcount}\" \"#{rating}\" \"#{last_played.chomp}\""
	end
end


def debug(msg)
	`dcop amarok playlist popupMessage 'MusinkSubmitter: #{msg}'`
end
	
def split_quotes (request)
		broken = request.scan(/\"[^\"]*\"/)
		i = 0 
		while (i < broken.length)
			broken[i] = broken[i].chomp[1..broken[i].chomp.length-2]
			i = i +1
		end
	return broken
end

def safe_for_sql(sqlstring)
	if (sqlstring==nil)
		return ""
	else
		sqlstring.gsub("'","''").gsub('"','\"').gsub("&#38;","&").gsub("&#62;",">").gsub("&#60;","<").strip
	end
end

def dcop_query(sqlquery)
	qback = `dcop amarok collection query \"#{sqlquery}\"`
	if sqlquery.length < 500
		#debug sqlquery
		#debug qback
	end
	return qback
end

def dcop(cmd)
	return `dcop amarok #{cmd}`
end

#Simplify the process of sending /receiving data from the sock, since it's a blocking server
def send_recv(in_string)
	@@sock = TCPSocket.new('localhost',34567) 
	@@sock.send(in_string,0) 		#send in_string to DBM
	data = String.new("")
	recv = String.new("")
	recv = @@sock.recv(512).strip
	@@sock.close
	return recv.strip	#return DBM response
end

def startup()
	config_file_str = "#{`echo ~`.chomp()}/.kde/share/apps/amarok/scripts-data/musink.config"
	
	setup_ratings_trigger()
	if FileTest.exist?(config_file_str) == false then
		resp = send_recv("\"marco\"\n")
		#resp_data = split_quotes(resp)
		#@id = resp_data[1]
		@@id = resp[8..resp.length-2]
		#debug("New Player ID: #{@id}")
		config_file = File.open(config_file_str,File::CREAT|File::RDWR)
		config_file.write @@id		#Write ID to musink.config 
		config_file.close
		new_player()			#Merge Library
	else
		config_file = File.open(config_file_str,File::RDWR)
		@@id = config_file.readlines[0]		#get old ID from file
		old_player()				#Get updates since last startup
	end
	return
end

# call this periodically to send ratings changes to the server
def check_ratings_changes()
	debug "checked ratings changes"
	change_list_raw = dcop_query("select * from ratings_changes")
	change_list = change_list_raw.split("\n")
	change_list.each {|rchange| 
		rchange_data = rchange.split(";")
		url = rchange_data[0]
		rating = rchange_data[1]
		song = get_song(url) 
		song.set_rating(rating)
		send_song(song)
	}
	dcop_query("delete from ratings_changes") # clear the table
end

# check ratings changes on termination
def cleanup()
	puts( "Script was terminated." )
	check_ratings_changes()
end

def setup_ratings_trigger()
	# record ratings changes in a table
	dcop_query("create table ratings_changes (url text, new_rating integer)")
	log_change = "insert into ratings_changes values (n.url, n.rating)"
	dcop_query("create trigger ratings_trigger after update on statistics "+
	           "REFERENCING NEW ROW AS n, OLD ROW AS o for each row "+
	           "if n.rating <> o.rating then #{log_change} end if;")

	# check the table every 10 minutes and when script terminates
	trap( "SIGTERM" ) { cleanup() }
	tid = Thread.new("ratings_checker") {
		while 1
			`sleep 600`
			check_ratings_changes()
		end
	}
end

# get a song object from a filename
def get_song(filename)
	fullpath = full_path(filename)
	response = send_recv("\"get\" \"#{fullpath}\"")
	response_data = split_quotes(response)

	s = Musink_Song.new(filename, response_data[1], response_data[2],
	                    response_data[3], response_data[4], response_data[5],
	                    response_data[6], response_data[7], response_data[8])
	#because DBM won't give us correct location, we don't use response_data[0]
	return s
end

# send a song object back, its data updated
def send_song(s)

	if (s.hash!="" and s.hash != nil)	#No need to fill the database with crap
		fullpath = full_path(s.filepath)
		#send_recv("\"set\" \"#{@id}\" \"#{fullpath}\" \"#{s.artist}\" \"#{s.album}\" \"#{s.title}\" \"#{s.duration}\" \"#{s.hash}\" \"#{s.playcount}\" \"#{s.rating}\" \"#{s.last_played}\"")
		send_recv("\"set\" \"#{@@id}\" #{s.to_response}")
	end
	return
end

def get_library()
	file_list_raw = dcop_query("Select url from tags")
	file_list = file_list_raw.split("\n")

	return file_list
end

def get_collection(file_list)
	song_list = Array.new
	file_list.each {|filename|
		song_list.push(get_song(filename))
	}
	return song_list
end

# get full filepath given relative path
def full_path(relative_path)
	rp = safe_for_sql(relative_path) # starts with "./"
	id = dcop_query("select deviceid from uniqueid where url='#{rp}'")
	lmp = dcop_query("select lastmountpoint from devices where id='#{id}'")
	

	relative_path = relative_path[1..relative_path.length-1] # remove the .
	lmp = lmp.chomp()
	if lmp == "/" || lmp == nil || lmp == ""
#		debug("#{relative_path}")
		return relative_path
	else
#		debug("#{lmp+relative_path}")
		return lmp+relative_path
	end
end

def update_data(song_list)
	new_list = Array.new
	song_list.each do |song|
		#debug song.filepath
		fp = safe_for_sql(song.filepath)

		artist_id = dcop_query("select artist from tags where url='#{fp}'")
		artist_raw = dcop_query("select name from artist where id='#{artist_id.strip()}'")
		song.set_artist( artist_raw.strip() )
		album_id = dcop_query("select album from tags where url='#{fp}'")
		album_raw = dcop_query("select name from album where id='#{album_id.strip()}'")
		song.set_album( album_raw.strip() )
		title_raw = dcop_query("select title from tags where url='#{fp}'")
		song.set_title( title_raw.strip() )
		length_raw = dcop_query("select length from tags where url='#{fp}'")
		song.set_duration(length_raw.chomp())

		pc_raw = dcop_query("select playcounter from statistics where url='#{fp}'")
		if (pc_raw.chomp() != nil) and (pc_raw.chomp() != "")
			song.increase_playcount( Integer(pc_raw.chomp()) )
		end
		rating_raw = dcop_query("select rating from statistics where url='#{fp}'")
		if (rating_raw.chomp() != nil) and (rating_raw.chomp() != "")
			song.set_rating(Float(rating_raw.chomp()))
		else
			song.set_rating(0.0)
		end
		lp_raw = dcop_query("select accessdate from statistics where url='#{fp}'")
		lp_a = time_amarok_to_musink(lp_raw.chomp())

		if song.last_played != nil and lp_a !="" and song.last_played < lp_a
			song.set_last_played(lp_a)
		end
		
		song.set_filepath(full_path(song.filepath))
		new_list << song

		# update amarok database to match "song"
		dcop_query("update statistics set rating=#{(song.rating)} where url=\'#{fp}\'")
		dcop_query("update statistics set playcounter=#{song.playcount} where url=\'#{fp}\'")
		lp_amarok = time_musink_to_amarok(song.last_played)
		dcop_query("update statistics set accessdate=#{lp_amarok} where url=\'#{fp}\'")
		# other fields were pulled straight from the database and don't need to be put back

	end
	return new_list
end

# convert from amarok's format to YYYYMMDD HH:mm:ss
# amarok's format is seconds since the epoch
def time_amarok_to_musink(a_time)
	t = Time.at(a_time.strip.to_i)
	return t.strftime("%Y%m%d %H:%M:%S")
end

# convert from musink's format (YYYYMMDD HH:mm:ss) to amarok's
def time_musink_to_amarok(m_time)
	m_time =~ /(\d{4})(\d\d)(\d\d) (\d\d):(\d\d):(\d\d)/
	t = Time.mktime($1,$2,$3,$4,$5,$6)
	return "#{t.to_i}"
end

def merge_to_database(song_list)
	conflicts = Array.new
	song_list.each do |song|
		conf = send_recv("\"new\" \"#{@@id}\" #{song.to_response}")	#Things work amazingly better with to_response (was missing the hash before)
		conf_data = split_quotes(conf)
		if (conf_data[0] == "conflict")
			c = [song, song.rating, conf_data[1]]
			conflicts << c
		end
	end
	return conflicts
end

def ask_conflict(song, a_rating, m_rating)
	dlg = KDialog.new
	dlg.menu("Ratings conflict with #{song.title} by #{song.artist} on #{song.album}.  Amarok rating: #{a_rating} Musink rating: #{m_rating}",
	   ["Use_Amarok_rating", "Use_Musink_rating", "Use_Amarok_ratings_for_all",
	    "Use_Musink_ratings_for_all"])
	dlg.selection.strip!()
	case dlg.selection
		when "Use_Amarok_rating"
			return ["amarok", "this"]
		when "Use_Musink_rating"
			return ["musink", "this"]
		when "Use_Amarok_ratings_for_all"
			return ["amarok", "all"]
		when "Use_Musink_ratings_for_all"
			return ["musink", "all"]
	end
end

def handle_conflicts(conflicts)
	conflict_updates = Array.new
	all = false
	choice = ""
	conflicts.each do |conflict|
		song = conflict[0]
		a_rating = conflict[1]
		m_rating = conflict[2]
		if !all # haven't said "apply (musink|amarok) rating to all"
			answer = ask_conflict(song, a_rating, m_rating)
			choice = answer[0] # musink or amarok
			if (answer[1] == "all")
				all = true
			end
		end
		if (choice == "amarok")
			song.set_rating(a_rating)
		else
			song.set_rating(m_rating)
		end
		send_song(song)
	end
end

def new_player()
	file_list = get_library()
	song_list = get_collection(file_list)
	song_list = update_data(song_list)
	conflicts = merge_to_database(song_list)
	handle_conflicts(conflicts)
	return
end

def old_player()
	output = "\"startup\" \"#{@@id}\""

	data = send_recv(output)
	changed_files = split_quotes(data)
	
	changed_files.each do |c_file|
		if (c_file!="" and c_file != nil)

			output = "\"geth\" \"#{c_file}\""		#use get song by hash function
			s_data = send_recv(output)


			#We're relying on the Artist, album, and track title to be consistant here... I don't like it, but we don't have much choice here

			if (s_data[0,5] != "Error:" )	#this shouldn't happen if the files havn't changed since the last time DBM loaded... 
						#but might be a problem when switching OSes
				s_vars = split_quotes(s_data)
				s = Musink_Song.new(s_vars[0],s_vars[1],s_vars[2],s_vars[3],s_vars[4],s_vars[5],s_vars[6],s_vars[7],s_vars[8]) #Song data from DBM

				# try to find the artist
				artist_id_str = dcop_query("select id from artist where name = \'#{safe_for_sql(s.artist)}\'").chomp
				artist_id = $1 if artist_id_str =~ /(\d+)/
			
				# try to find the album
				album_id_str = dcop_query("select id from album where name = \'#{safe_for_sql(s.album)}\'").chomp
				album_id = $1 if album_id_str =~ /(\d+)/
			
				#These may need to be changed
				yield "artist id not found:  \"#{s.artist}\" - skipping" if artist_id.nil?
				next if artist_id.nil?
			
				yield "album id not found:    \"#{s.album}\" - skipping" if album_id.nil?
				next if album_id.nil?
			
				# only find the song, if both album and artist are known
				url_str = dcop_query("select url from tags where title = \'#{safe_for_sql(s.title)}\' and artist = #{artist_id} and album = #{album_id}").chomp
				url = $1 if url_str =~ /(.+)/
			
				yield "Unable to update: URL not found for \"#{s.title}\"" if url.nil?
				next if url.nil?
			
				# if the song was already played, the statistics should be updated.
				# otherwise insert a new statistics entry
				result_str = ""
				found_str = dcop_query("select playcounter from statistics where url=\'#{safe_for_sql(url)}\'")
				lp_amarok = time_musink_to_amarok(s.last_played)
				if found_str =~ /(.+)/
					result_str = dcop_query("update statistics set rating=#{(s.rating)} where url=\'#{safe_for_sql(url)}\'")
					dcop_query("update statistics set playcounter=#{s.playcount} where url=\'#{safe_for_sql(url)}\'")
					dcop_query("update statistics set accessdate=#{lp_amarok} where url=\'#{safe_for_sql(url)}\'")
				else
					result_str = dcop_query("insert into statistics (url, createdate, accessdate, percentage, playcounter, rating) values(\'#{safe_for_sql(url)}\',#{s.last_played},#{lp_amarok},#{s.rating},#{s.playcount},#{s.rating})")
				end
			
				result = $1 if result_str =~ /(.+)/
				yield result if !result.nil?

			else
				#Code to ask User where the file might be located should be here
			end
		end
		
	end
	#debug "Finished old_player"
end




################################
# Main Program                 #
################################
	#@@sock = TCPSocket.new('localhost',34567) #Will eventually need to be changed to handle port stepping

	startup()

	loop do
		
    		message = gets().chomp() #Read message from stdin
    		line = /[A-Za-z]*/.match( message ).to_s()
		
#		`dcop amarok playlist popupMessage "#{line}"`			#Pop up a window in amarok so I know what got scanned
		
		case line
			when 'configure' then AmaroKSlots.configure
			when 'trackChange' then AmaroKSlots.trackChange
			when 'engineStateChange: empty' then AmaroKSlots.stateChange_empty
			when 'engineStateChange: idle' then AmaroKSlots.stateChange_idle
			when 'engineStateChange: paused' then AmaroKSlots.stateChange_paused
			when 'engineStateChange: playing' then AmaroKSlots.stateChange_playing
			#not a command given by amaroK, helpful when debugging
			when 'exit' then AmaroKSlots.exit
			else $stderr.print "Illegal command: #{line}"
		end	
    	end

